﻿using SuperGo.SilverlightLib.History;
using System;
using System.Collections.Generic;

namespace SuperGo.SilverlightLib {
	public class GoGame {
        private readonly GameHistory _history;
        private bool _inKoCheck;
		private readonly int _handicap;
		private readonly decimal _komi;

        public delegate void MoveMadeDelegate(GoGame source, int x, int y, Player player, int captures);
        public event MoveMadeDelegate MoveMade;

        public delegate void PlayerPassedDelegate(GoGame source, Player player);
        public event PlayerPassedDelegate PlayerPassed;

		public delegate void CaptureMadeDelegate(GoGame source, int x, int y);
		public event CaptureMadeDelegate CaptureMade;

		public delegate void RedrawDelegate(GoGame source);
		public event RedrawDelegate Redraw;

		public delegate void DeadStonesMarkedDelegate(GoGame source, IList<Intersection> deadStones);
		public event DeadStonesMarkedDelegate DeadStonesMarked;

		public delegate void GameStateChangedDelegate(GoGame source, GameState gameState);
		public event GameStateChangedDelegate GameStateChanged;

		public delegate void PointsIdentifiedDelegate(GoGame source, BitMatrix blackPoints, BitMatrix whitePoints);
		public event PointsIdentifiedDelegate PointsIdentified;

		private readonly BitMatrix _deadStones;

		public BitMatrix CurrentDeadStones {
			get { return _deadStones; }
		}

		public BitMatrix CurrentBlackPoints { get; private set; }
		public BitMatrix CurrentWhitePoints { get; private set; }

		public Player WhitePlayer {
			get { return Player.White; }
		}

		public Player BlackPlayer {
			get { return Player.Black; }
		}

		public GameHistory History {
			get { return _history; }
		}

		public int Handicap {
			get { return _handicap; }
		}

		public GoGame(int size, int handicap, decimal komi, string whiteUsername, string blackUsername) {
			// white should go first if there is a handicap
			Player whoGoesFirst = handicap > 1 ? Player.White : Player.Black;

			_handicap = handicap;
			_komi = komi;
			_history = new GameHistory(size, whoGoesFirst);
			_deadStones = new BitMatrix(size);
			PlayHandicaps(handicap);
		}

		public static int GetMaxHandicap(int boardSize) {
			return Board.GetStarPoints(boardSize).Length;
		}

		private void PlayHandicaps(int handicap) {
			if (handicap <= 1)
				return;
			handicap = Math.Min(handicap, GetMaxHandicap(Board.Size));

			int[,] handicaps = Board.GetHandicaps(Board.Size, handicap);

			for (int i = 0; i < handicap; i++) {
				PregamePass();
				PregamePlay(handicaps[i, 0], handicaps[i, 1]);
			}
		}

        public Board Board {
            get { return _history.CurrentBoard; }
        }

        public GameState GameState {
            get { return _history.CurrentGameState; }
        }

		public Intersection LastMove {
		    get {
		    	if (_history.CurrentHistoryEvent is MoveHistoryEvent) {
		    		MoveHistoryEvent historyEvent = (MoveHistoryEvent) _history.CurrentHistoryEvent;
		    		return Board[historyEvent.X, historyEvent.Y];
		    	}
		    	return null;
		    }
		}

        public Player CurrentPlayer {
            get { return _history.CurrentPlayer; }
        }

        public int BlackCaptures {
            get { return _history.CurrentBlackCaptures; }
        }

        public int WhiteCaptures {
            get { return _history.CurrentWhiteCaptures; }
        }

        public int Turn {
            get { return _history.Turn; }
        }

        public void Resign() {
            if (CurrentPlayer == null)
				throw new GoException("Cannot pass when it is neither white or black's turn.");

            _history.AddResign();
			SendGameStateChangedEvent();
            //_history.AddGameOver();
        }

		public void Pass() {
			Pass(true, true);
		}

		internal void PregamePass() {
			Pass(false, false);
		}

		private void MarkAsDead(int x, int y) {
			if (!(_history.CurrentHistoryEvent is NegotiatingDeadStonesHistoryEvent))
				throw new GoException("Can only mark as dead after two passes.");

			IList<Intersection> deadGroup = GetGroup(x, y);

			foreach (Intersection intersection in deadGroup) {
				int intX = intersection.X;
				int intY = intersection.Y;
				_deadStones[intX, intY] = !_deadStones[intX, intY];
			}

			if (DeadStonesMarked != null)
				DeadStonesMarked(this, deadGroup);
		}

		private void Pass(bool storeInHistory, bool fireEvents) {
            if (CurrentPlayer == null)
				throw new GoException("Cannot pass when it is neither white or black's turn.");
			if (GameState == GameState.NegotiatingDeadStones)
				throw new GoException("Cannot pass when negotiating dead stones.");

			Player player = CurrentPlayer;

            bool timeToNegotiateDeadStones = _history.LastMovePass;

            // add to history and send message
			_history.AddPass(storeInHistory);
			if (PlayerPassed != null && fireEvents) {
				PlayerPassed(this, player);
            }

            if (timeToNegotiateDeadStones) {
				_history.AddNegotiateDeadStones();
            }

			if (fireEvents)
				SendGameStateChangedEvent();
		}

		private void SendGameStateChangedEvent() {
			if (GameStateChanged != null) {
				GameStateChanged(this, GameState);
			}
		}

		public void Undo(bool fireEvents) {
			_history.Undo();
			if (fireEvents && Redraw != null)
				Redraw(this);
			if (fireEvents)
				SendGameStateChangedEvent();
		}

		public void Undo() {
			Undo(true);
		}

		public void Play(int x, int y) {
			CurrentBlackPoints = null;
			CurrentWhitePoints = null;

			if (GameState == GameState.NegotiatingDeadStones)
				MarkAsDead(x, y);
			else
				Play(Board.Clone(), CurrentPlayer, x, y, true, true);
        }

		internal void PregamePlay(int x, int y) {
			Play(Board.Clone(), CurrentPlayer, x, y, true, false);
		}

        private void Play(Board board, Player player, int x, int y, bool fireEvents, bool storeInHistory) {
            InvalidMoveReason reason;
            if (!IsValidMove(board, player, x, y, out reason)) {
                throw new GoException(string.Format("Location {0} is invalid. {1}", Intersection.ToString(x, y),
                                                    reason));
            }

            board[x, y].State = player.ToStoneState();
            int captures = RemoveCaptures(board, x, y, player, fireEvents);

            // add to history and send message
			_history.AddMove(board, x, y, player, captures, storeInHistory);
			if (fireEvents && MoveMade != null) {
				MoveMade(this, x, y, player, captures);
				SendGameStateChangedEvent();
			}
        }

		public IList<Intersection> GetGroup(int x, int y) {
			bool bordersBlack;
			bool bordersWhite;
			bool bordersEmpty;
			
			return GetGroup(x, y, out bordersBlack, out bordersWhite, out bordersEmpty);
		}

		public IList<Intersection> GetGroup(int x, int y, out bool bordersBlack, out bool bordersWhite, out bool bordersEmpty) {
			bordersBlack = false;
			bordersWhite = false;
			bordersEmpty = false;

			IList<Intersection> intersections = new List<Intersection>();
			GetGroup(x, y, Board, Board[x, y].State, intersections, ref bordersBlack, ref bordersWhite, ref bordersEmpty);

			return intersections;
		}

		private static void GetGroup(int x, int y, Board board, StoneState compareToState, ICollection<Intersection> intersections, ref bool bordersBlack, ref bool bordersWhite, ref bool bordersEmpty) {
			bool stoneIsOutsideBoard = x < 0 || x >= board.Size || y < 0 || y >= board.Size;
			if (stoneIsOutsideBoard)
				return;

			Intersection currentIntersection = board[x, y];
			bool stoneIsInGroup = currentIntersection.State == compareToState;
			bool stoneAlreadyAdded = intersections.Contains(currentIntersection);

			if (stoneIsInGroup && !stoneAlreadyAdded) {
				intersections.Add(currentIntersection);

				GetGroup(x + 1, y, board, compareToState, intersections, ref bordersBlack, ref bordersWhite, ref bordersEmpty);
				GetGroup(x - 1, y, board, compareToState, intersections, ref bordersBlack, ref bordersWhite, ref bordersEmpty);
				GetGroup(x, y + 1, board, compareToState, intersections, ref bordersBlack, ref bordersWhite, ref bordersEmpty);
				GetGroup(x, y - 1, board, compareToState, intersections, ref bordersBlack, ref bordersWhite, ref bordersEmpty);
			}
			
			if (!stoneIsInGroup) {
				if (currentIntersection.State == StoneState.Black)
					bordersBlack = true;
				if (currentIntersection.State == StoneState.White)
					bordersWhite = true;
				if (currentIntersection.State == StoneState.Empty)
					bordersEmpty = true;
			}
		}

		private void MergeStonesMarkedAsDead(bool fireEvents) {
			IList<Intersection> blackCaptures;
			IList<Intersection> whiteCaptures;
			GetCapturesFromMarkedDeadStones(out blackCaptures, out whiteCaptures);

			_history.AddGameOver(whiteCaptures.Count, blackCaptures.Count);

			MakeCaptures(blackCaptures, fireEvents);
			MakeCaptures(whiteCaptures, fireEvents);

			if (fireEvents)
				SendGameStateChangedEvent();
		}

		public void MergeStonesMarkedAsDead() {
			MergeStonesMarkedAsDead(true);
		}

		/// <summary>
		/// This version really just exists to call the PointsIdentified event.
		/// </summary>
		public void CalculateScore() {
			decimal blackScore;
			decimal whiteScore;
			CalculateScore(out blackScore, out whiteScore);
		}

		public void CalculateScore(out decimal blackScoreFinal, out decimal whiteScoreFinal) {
			bool undoAfterCalculation = false;
			if (GameState == GameState.NegotiatingDeadStones) {
				MergeStonesMarkedAsDead(false);
				undoAfterCalculation = true;
			}

			int size = Board.Size;

			BitMatrix blackPoints = new BitMatrix(size);
			BitMatrix whitePoints = new BitMatrix(size);

			int blackScore = 0;
			int whiteScore = 0;

			BitMatrix visitedIntersections = new BitMatrix(size);
			
			for (int y = 0; y < size; y++) {
				for (int x = 0; x < size; x++) {
					FindPointsAtIntersection(y, x, visitedIntersections, blackPoints, whitePoints, ref blackScore, ref whiteScore);
				}
			}

			blackScore += BlackCaptures;
			whiteScore += WhiteCaptures;

			if (undoAfterCalculation)
				Undo(false);

			if (PointsIdentified != null)
				PointsIdentified(this, blackPoints, whitePoints);
			CurrentBlackPoints = blackPoints;
			CurrentWhitePoints = whitePoints;

			blackScoreFinal = blackScore;
			whiteScoreFinal = whiteScore + _komi;
		}

		private void FindPointsAtIntersection(int y, int x, BitMatrix visitedIntersections, BitMatrix blackPoints, BitMatrix whitePoints, ref int blackScore, ref int whiteScore) {
			Intersection intersection = Board[x, y];
			bool isEmpty = intersection.State == StoneState.Empty;
			bool isVisited = visitedIntersections[x, y];

			// only investigate non-empty unprocessed groups
			if (!isEmpty || isVisited) return;

			// get the intersections in the group
			bool bordersBlack;
			bool bordersWhite;
			bool bordersEmpty;
			IList<Intersection> group = GetGroup(x, y, out bordersBlack, out bordersWhite, out bordersEmpty);

			bool bordersOnlyBlackOrOnlyWhite = (bordersBlack && !bordersWhite) || (bordersWhite && !bordersBlack);

			// only count groups that are bordered exclusively either black or white (not both)
			if (!bordersOnlyBlackOrOnlyWhite) return;

			if (bordersBlack) {
				foreach (Intersection blackPoint in group)
					blackPoints[blackPoint.X, blackPoint.Y] = true;
				blackScore += group.Count;
			}
			if (bordersWhite) {
				foreach (Intersection whitePoint in group)
					whitePoints[whitePoint.X, whitePoint.Y] = true;
				whiteScore += group.Count;
			}

			foreach (Intersection visitIntersection in group) {
				visitedIntersections[visitIntersection.X, visitIntersection.Y] = true;
			}
		}

		private void MakeCaptures(IEnumerable<Intersection> capturesToMake, bool fireEvents) {
			foreach (Intersection intersection in capturesToMake) {
				int x = intersection.X;
				int y = intersection.Y;

				Board[x, y].State = StoneState.Empty;
				if (fireEvents && CaptureMade != null)
					CaptureMade(this, x, y);
			}
		}

		private void GetCapturesFromMarkedDeadStones(
			out IList<Intersection> blackCaptures, 
			out IList<Intersection> whiteCaptures
			) {

			blackCaptures = new List<Intersection>();
			whiteCaptures = new List<Intersection>();

			for (int y = 0; y < Board.Size; y++) {
				for (int x = 0; x < Board.Size; x++) {
					bool markedAsDead = _deadStones[x, y];
					if (markedAsDead) {
						Intersection intersection = Board[x, y];
						StoneState existingState = intersection.State;

						if (existingState == StoneState.Black)
							blackCaptures.Add(intersection);
						else if (existingState == StoneState.White)
							whiteCaptures.Add(intersection);
						else
							throw new Exception("Cannot mark an empty space as a capture.");
					}
				}
			}
		}

		/// <summary>
        /// Removes possible captures. Look up, down, left, right for stones of a different color
        /// from the player. If they have zero liberties remove them.
        /// </summary>
        private int RemoveCaptures(Board board, int x, int y, Player player, bool fireEvents) {
            int captures = 0;
            captures += RemoveCapturesInternal(board, x - 1, y, player, fireEvents);
            captures += RemoveCapturesInternal(board, x + 1, y, player, fireEvents);
            captures += RemoveCapturesInternal(board, x, y - 1, player, fireEvents);
            captures += RemoveCapturesInternal(board, x, y + 1, player, fireEvents);
            return captures;
        }

        private int RemoveCapturesInternal(Board board, int x, int y, Player player, bool fireEvents) {
            if (x < 0 || x >= board.Size || y < 0 || y >= board.Size) {
                return 0;
            }
            // if it's an opponents stone check for liberties and remove.
            if (board[x, y].State != player.ToStoneState() && GetLiberties(board, x, y) == 0) {
                return RemoveGroupAsCaptures(board, x, y, board[x, y].State, fireEvents);
            }
            return 0;
        }

        private int RemoveGroupAsCaptures(Board board, int x, int y, StoneState typeToRemove, bool fireEvents) {
            if (x < 0 || x >= board.Size || y < 0 || y >= board.Size) {
                return 0;
            }
            if (board[x, y].State == typeToRemove) {
                int captures = 1;
                board[x, y].State = StoneState.Empty;
                if (fireEvents && CaptureMade != null) {
                    CaptureMade(this, x, y);
                }
                captures += RemoveGroupAsCaptures(board, x - 1, y, typeToRemove, fireEvents);
                captures += RemoveGroupAsCaptures(board, x + 1, y, typeToRemove, fireEvents);
                captures += RemoveGroupAsCaptures(board, x, y - 1, typeToRemove, fireEvents);
                captures += RemoveGroupAsCaptures(board, x, y + 1, typeToRemove, fireEvents);
                return captures;
            }
            return 0;
        }

        /// <summary>
        /// Check that we are on the board.
        /// Check that the spot is open.
        /// Check that it's not suicide to move there.
        /// </summary>
        public bool IsValidMove(Board board, Player player, int x, int y, out InvalidMoveReason reason) {
			if (x < 0 || x >= board.Size || y < 0 || y >= board.Size) {
				reason = InvalidMoveReason.OutsideOfBoard;
				return false;
			}
			if (GameState == GameState.NegotiatingDeadStones) {
				if (board[x, y].State == StoneState.Empty) {
					reason = InvalidMoveReason.CanNotMarkEmptySpaceAsDead;
					return false;
				}
				reason = InvalidMoveReason.Ok;
				return true;
			}
			if (GameState == GameState.BlackResigned || GameState == GameState.WhiteResigned || GameState == GameState.DeadStonesMergedGameOver) {
                reason = InvalidMoveReason.GameOver;
				return false;
			}
            if (board[x, y].State != StoneState.Empty) {
                reason = InvalidMoveReason.SpaceNotEmpty;
				return false;
			}
            if (IsSuicide(board, player, x, y)) {
                reason = InvalidMoveReason.Suicide;
				return false;
			}
            if (IsKo(board, player, x, y)) {
                reason = InvalidMoveReason.Ko;
            	return false;
            }

			reason = InvalidMoveReason.Ok;
			return true;
		}

        private bool IsKo(Board board, Player player, int x, int y) {
            try {
                if (!_inKoCheck) {
                    _inKoCheck = true;
                    Board tempBoard = board.Clone();
                    Play(tempBoard, player, x, y, false, true);
                    _history.Undo();
                    foreach (HistoryEvent historyEvent in _history.EventFromLastToFirst()) {
                        if (historyEvent is MoveHistoryEvent || historyEvent is PassHistoryEvent) {
                            if (tempBoard.Equals(historyEvent.Board)) {
                                return true;
                            }
                        }
                    }
                }
            }
            finally {
                _inKoCheck = false;
            }
            return false;
        }

        public bool IsValidMove(int x, int y) {
            InvalidMoveReason reason;
            return IsValidMove(x, y, out reason);
        }

        public bool IsValidMove(int x, int y, out InvalidMoveReason reason) {
            return IsValidMove(CurrentPlayer, x, y, out reason);
        }

        public bool IsValidMove(Player player, int x, int y) {
            InvalidMoveReason reason;
            return IsValidMove(player, x, y, out reason);
        }

        public bool IsValidMove(Player player, int x, int y, out InvalidMoveReason reason) {
            Board board = _history.CurrentBoard;
            return IsValidMove(board, player, x, y, out reason);
        }

        /// <summary>
        /// Check up, down, left, right for suicide.
        /// </summary>
        private bool IsSuicide(Board board, Player player, int x, int y) {
            // test left
            if (x - 1 >= 0 && IsSuicideTestInternal(board, player, x - 1, y) == false) {
                return false;
            }

            // test right
            if (x + 1 < board.Size && IsSuicideTestInternal(board, player, x + 1, y) == false) {
                return false;
            }

            // test up
            if (y - 1 >= 0 && IsSuicideTestInternal(board, player, x, y - 1) == false) {
                return false;
            }

            // test down
            if (y + 1 < board.Size && IsSuicideTestInternal(board, player, x, y + 1) == false) {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Test for suicide, this is called from IsSuicide so this is either the square up, down,
        /// left, right from the spot to check.
        /// If it's empty it's not suicide.
        /// If the spot is occupied by the players stone and it has liberties it's not suicide.
        /// If this spot is occupied by the opponent's stone and it has less than one liberty it's not suicide.
        /// </summary>
        private bool IsSuicideTestInternal(Board board, Player player, int x, int y) {
            // spot empty
            if (board[x, y].State == StoneState.Empty) {
                return false;
            }
            if (board[x, y].State == player.ToStoneState()) {
                // neighbor has at least 1 liberties, which if we play here will result in at least 1 liberty).
                if (GetLiberties(board, x, y) > 1) {
                    return false;
                }
            }
            else {
                // opponent has 1 liberties (which will be filled in if we play here), capture.
                if (GetLiberties(board, x, y) <= 1) {
                    return false;
                }
            }
            return true;
        }

        public int GetLiberties(Board board, int x, int y) {
            bool[,] visited = new bool[board.Size, board.Size];
            for (int initX = 0; initX < board.Size; initX++) {
                for (int initY = 0; initY < board.Size; initY++) {
                    visited[initX, initY] = false;
                }
            }
            return GetLibertiesInternal(board, x, y, board[x, y].State, visited);
        }

        /// <summary>
        /// Checks for liberties.
        /// Walks the group and gets the number of liberties recursivly.
        /// </summary>
        private static int GetLibertiesInternal(Board board, int x, int y, StoneState stoneState, bool[,] visited) {
            if (x < 0 || y < 0 || x >= board.Size || y >= board.Size) {
                return 0;
            }
            if (visited[x, y]) {
                return 0;
            }
            visited[x, y] = true;
            if (board[x, y].State == StoneState.Empty) {
                return 1;
            }
        	if (board[x, y].State != stoneState) {
        		return 0;
        	}
        	
			return GetLibertiesInternal(board, x - 1, y, stoneState, visited) +
        	       GetLibertiesInternal(board, x + 1, y, stoneState, visited) +
        	       GetLibertiesInternal(board, x, y - 1, stoneState, visited) +
        	       GetLibertiesInternal(board, x, y + 1, stoneState, visited);
        }

		public void SetStone(int x, int y, StoneState state) {
			// TODO: add history events?  This is called by the SGF import to setup up board state. This may include handicap stones.
			Board[x, y].State = state;
		}

		public void ResetDeadGroups() {
			if (GameState != GameState.NegotiatingDeadStones)
				return;

			_deadStones.Initialize();
		}
	}
}
